// -------------------------------------------------------------------------------------
// ScrollText.m
// author: Martin D. Flynn
// (see ScrollText.h for usage information)
// -------------------------------------------------------------------------------------
// Permission is granted to freely redistribute this source code, and to use fragments
// of this code in your own applications if you find them to be useful.  This class,
// along with the source code, come with no warranty of any kind, and the user assumes
// all responsibility for its use.
// -------------------------------------------------------------------------------------

#import <appkit/appkit.h>
#import <libc.h>
#import <mach/cthreads.h>
#import <stdlib.h>
#import <stdarg.h>
#import <string.h>
#import <pwd.h>
#import <sys/types.h>
#import <sys/wait.h>
#import "ScrollText.h"

// -------------------------------------------------------------------------------------
// mutex defines
#define	mutexAlloc		mutex_alloc
#define	mutexFree		mutex_free
#define	mutexLock		mutex_lock
#define	mutexUnlock		mutex_unlock

// -------------------------------------------------------------------------------------
// null text attribute structure
static NXColor			nullColor = { 0 };
static textAttr_t		nullAttr = { (id)nil, 0 };
#define	isNullAttr(X)	(!X.fontId && !X.colorMode)

// -------------------------------------------------------------------------------------
// main thread handle
#define	isMainTHREAD	(mainThread == cthread_self())
static cthread_t        mainThread = (cthread_t)nil;

// -------------------------------------------------------------------------------------
// Should be proto-typed elsewhere
@interface Object(ShouldBeElsewhere)
- (const char*)printerName;	// should be in PrintInfo
@end

// -------------------------------------------------------------------------------------
// Object thread support
@interface Object(ThreadPerform)
- mainThreadPerform:(SEL)aSelector with:anArg wait:(BOOL)wait;
@end

// -------------------------------------------------------------------------------------
// Object notified when command completes
@interface Object(ScrollTextDelegate)
- commandDidComplete:shellId withError:(int)errorCode;
@end

// -------------------------------------------------------------------------------------
// private methods
@interface ScrollText(Private)
- _setTextAttrFont:fontId color:(int)mode:(NXColor)color;
- (BOOL)_appendTextToView:(const char*)buffer len:(int)len attr:(textAttr_t)tAttr;
- _appendTextAndMakeVisible:(const char*)buffer attr:(textAttr_t)tAttr;
- _appendTextToQueue:(const char*)buffer attr:(textAttr_t)tAttr;
- _appendQueue:sender;
- _delayedAppendQueue;
- (int)_queueLength:(textQueue_t*)queue;
- (int)_maxRunLength:(textQueue_t*)queue;
- _stopCommand;
- (int)_popen:(const char*)cmd cmdPath:(const char*)cmdPath;
- (int)_pclose;
- (void)_gotData;
@end

// -------------------------------------------------------------------------------------
/* convert color to gray */
static float cvtColor2Gray(NXColor color)
{
	float	gray;
	NXConvertColorToGray(color, &gray);
	return gray;
}

// -------------------------------------------------------------------------------------
@implementation ScrollText

// -------------------------------------------------------------------------------------
/* initializes the outlet (to be executed by main thread only!) */
+ newScrollText:anObject
{

	/* check thread */
	if (!mainThread) mainThread = cthread_self();
	
	/* init */
	self			 = [super new];
	scrollView		 = anObject;
	textView		 = [scrollView docView];
	wasEditable		 = [textView isEditable];
	autoLf			 = NO;
	queueMutex		 = mutexAlloc();
	queueData		 = (textQueue_t*)nil;
	queueBack		 = (textQueue_t*)nil;
	runAttr			 = nullAttr;
	cmdChild		 = 0;
	
	/* set textView attributes */
	//[textView setEditable:NO];
	//[textView setMonoFont:NO];
	
	return self;
}

/* return textView id (docView) */
- docView
{
	return textView;
}

/* return scroll view */
- scrollView
{
	return scrollView;
}

/* set delegate */
- setDelegate:theDelegate
{
	delegate = theDelegate;
	return self;
}

/* set auto linefeed mode */
- setAutoLineFeed:(BOOL)mode
{
	autoLf = mode;
	return self;
}

/* free object */
- free
{

	/* kill any running child process */
	[self killCommand];

	/* free queueData */
	for (;queueData;) {
		textQueue_t	*next = queueData->next;
		if (queueData->record) free(queueData->record);
		free(queueData);
		queueData = next;
	}
	mutexFree(queueMutex);
	
	/* free super */
	return [super free];
	
}

// --------------------------------------------------------------------------------

/* set font */
- _setTextAttrFont:fontId color:(int)mode:(NXColor)color
{
	textAttr_t	tAttr = nullAttr;
	
	/* init text attributes */
	tAttr.fontId	= fontId;
	tAttr.colorMode	= mode;
	tAttr.color		= color;
	[self _appendTextAndMakeVisible:(char*)nil attr:tAttr];
	
	return self;
}

/* set font */
- setTextAttributeFont:fontId
{
	return [self _setTextAttrFont:fontId color:0:nullColor];
}

/* set gray */
- setTextAttributeGray:(float)aGray
{
	return [self _setTextAttrFont:(id)nil color:1:NXConvertGrayToColor(aGray)];
}

/* set gray */
- setTextAttributeColor:(NXColor)aColor
{
	return [self _setTextAttrFont:(id)nil color:2:aColor];
}

/* set default tabs */
// THIS NEEDS TO BE REDONE
- setTabStops:(float*)tabArray count:(int)c
{
	NXTextStyle	style = *((NXTextStyle*)[textView defaultParaStyle]);
	style.numTabs = (short)c;
	style.tabs = (NXTabStop*)malloc(sizeof(NXTabStop) * style.numTabs);
	while (--c >= 0) { style.tabs[c].kind = NX_LEFTTAB; style.tabs[c].x = tabArray[c]; }
	[textView setParaStyle:(void*)&style];
	return self;
}

/* repeat given tab multiple times */
- setTab:(float)tabSize count:(int)c
{
	int			i;
	NXTextStyle	style = *((NXTextStyle*)[textView defaultParaStyle]);
	style.numTabs = (short)c;
	style.tabs = (NXTabStop*)malloc(sizeof(NXTabStop) * style.numTabs);
	for (i = 0; i < c; i++) {
		style.tabs[i].kind = NX_LEFTTAB;
		style.tabs[i].x = (float)(i + 1) * tabSize;
	}
	[textView setParaStyle:(void*)&style];
	return self;
}

// --------------------------------------------------------------------------------

/* clear text scroll view area */
- clearScrollText
{
	[textView setEditable:YES];
	[textView setText:""];
	if (!wasEditable) [textView setEditable:NO];
	[scrollView display];
	return self;
}
- clear:sender
{
	return [self clearScrollText];
}

/* delete lines */
- deleteLinesFrom:(int)fLine to:(int)tLine
{
	[textView setEditable:YES];
	[textView setSel:[textView positionFromLine:fLine]:[textView positionFromLine:tLine+1]];
	[textView replaceSel:""];
	if (!wasEditable) [textView setEditable:NO];
	[scrollView display];
	return self;
}

/* return current number of lines */
- (int)textLines
{
	return [textView lineFromPosition:[textView textLength]];
}
    
// --------------------------------------------------------------------------------
// append text to scroll view

/* append buffer to view: return YES if text was visible */
- (BOOL)_appendTextToView:(const char*)buffer len:(int)len attr:(textAttr_t)tAttr
{
	int		txtLen;
	NXSelPt	startPt, endPt;
	NXRect	rect;
	
	/* check for font/gray change (save state) */
	if (tAttr.fontId) {
		runAttr.fontId = tAttr.fontId;
	}
	if (tAttr.colorMode) {
		runAttr.colorMode = tAttr.colorMode;
		runAttr.color = tAttr.color;
	}
	if (!buffer || !*buffer || (len == 0)) return NO;
	
	/* get ready to print text */
	[textView getVisibleRect:&rect];                      // visible rectangle
	[textView setEditable:YES];
	txtLen = [textView textLength];
	[textView setSel:txtLen :txtLen];
	[textView getSel:&startPt :&endPt];                   // selected coordinates

	/* set text run attributes if specified */
	if (!isNullAttr(runAttr)) {
		if ([textView isMonoFont]) [textView setMonoFont:NO];
		if (!txtLen) { [textView replaceSel:" "]; [textView setSel:0 :1]; }
		if (runAttr.fontId) [textView setSelFont:runAttr.fontId];
		if (runAttr.colorMode == 1) [textView setSelGray:cvtColor2Gray(runAttr.color)]; else
		if (runAttr.colorMode == 2) [textView setSelColor:runAttr.color];
		runAttr = nullAttr;
	}
	
	/* print text */
	if (len > 0) [textView replaceSel:buffer length:len];
	else [textView replaceSel:buffer];
	if (!wasEditable) [textView setEditable:NO];
	
	return (rect.origin.y + rect.size.height > endPt.y);  // was visible?
}

/* append text to view and scroll to visible */
- _appendTextAndMakeVisible:(const char*)buffer attr:(textAttr_t)tAttr
{
	BOOL	wasVisible;
  
	/* if not main thread, append buffer to queue */
	if (!isMainTHREAD) return [self _appendTextToQueue:buffer attr:tAttr];

	/* print queue contents */
	[self _appendQueue:self];
  
	/* print buffer */
	wasVisible = [self _appendTextToView:buffer len:-1 attr:tAttr];
	if (autoLf && buffer) [self _appendTextToView:"\n" len:-1 attr:nullAttr];
	if (wasVisible) [textView scrollSelToVisible];
  
	return self;
}
    
// --------------------------------------------------------------------------------
// queue up text to print / print text queue

/* return queue length */
- (int)_queueLength:(textQueue_t*)queue
{
	int			len;
	textQueue_t	*qPtr;
	qPtr = (queue)? queue: queueData;
	for (len = 0; qPtr; qPtr = qPtr->next) {
		if (qPtr->record) len += strlen(qPtr->record) + 1;
	}
	return len;
}

/* return queue length */
- (int)_maxRunLength:(textQueue_t*)queue
{
	int			len, maxLen;
	textQueue_t	*qPtr;
	qPtr = (queue)? queue: queueData;
	for (maxLen = len = 0; qPtr; qPtr = qPtr->next) {
		if (!isNullAttr(qPtr->attr)) {
			if (len > maxLen) maxLen = len;
			len = 0;
		}
		if (qPtr->record) len += strlen(qPtr->record) + 1;
	}
	return MAX(maxLen, len);
}

/* delayed append queue */
- _delayedAppendQueue
{
	if ([self respondsTo:@selector(mainThreadPerform:with:wait:)])
		[self mainThreadPerform:@selector(_appendQueue:) with:self wait:NO];
	return self;
}

/* add text string to queue (to be executed by non-main thread only) */
- _appendTextToQueue:(const char*)buffer attr:(textAttr_t)tAttr
{
	textQueue_t	*newData;

	/* load temporarily permanent buffer */
	newData         = (textQueue_t*)malloc(sizeof(textQueue_t));
	newData->record = buffer? NXCopyStringBuffer(buffer) : (char*)nil;
	newData->attr	= tAttr;
	newData->next   = (textQueue_t*)nil;
  
	/* lock the queue */
	mutexLock(queueMutex);
  
	/* add it to the queue */
	if (!queueData) { queueData = newData; [self _delayedAppendQueue]; }
	else queueBack->next = newData;
	queueBack = newData;

	/* unlock the queue */
	mutexUnlock(queueMutex);
  
	return self;
}

/* print text queue contents (executed by main thread only!) */
// needs to be fixed to support fonts/grays
- _appendQueue:sender
{
	int			i;
	char		*text;
	textQueue_t	*queue, *next;
  
	/* get pointer to queue list */
	mutexLock(queueMutex);
	queue = queueData;
 	queueData = queueBack = (textQueue_t*)nil;
	mutexUnlock(queueMutex);
	if (!queue) return self;
  
	/* concatenate text */
	text = (char*)malloc([self _maxRunLength:queue] + 1);
	for (i = 0; queue;) {
	
		/* check for attribute change */
		if (!isNullAttr(queue->attr)) {	// print contents of 'text'
			if ((i > 0) && [self _appendTextToView:text len:-1 attr:nullAttr])
				[textView scrollSelToVisible];
			[self _appendTextToView:"" len:0 attr:queue->attr];
			text[i = 0] = 0;
		}
		
		/* append record to string buffer */
		if (queue->record) {
			i += sprintf(&text[i], "%s%s", queue->record, ((autoLf)? "\n": ""));
			free(queue->record);
		}
		
		/* next node */
		next = queue->next;
		free(queue);
		queue = next;
		
	}
  
	/* place remaining text into scroll view */
	if ([self _appendTextToView:text len:-1 attr:nullAttr]) [textView scrollSelToVisible];
	free(text);
    
	return self;
}

// --------------------------------------------------------------------------------
// append a formatted text string message into a text view
// input from ktaylor

/* append text unformatted */
- (int)textPrint:(const char*)buffer
{
	[self _appendTextAndMakeVisible:buffer attr:nullAttr];
	return strlen(buffer);
}

/* append text with variable args into textView */
- (int)textPrintf:(const char*)fmt args:(va_list)args
{
	char	tempString[textStringSIZE] = { 0 };
	int		retVal = vsprintf(tempString, fmt, args);
	[self _appendTextAndMakeVisible:tempString attr:nullAttr];
	return retVal;
}

/* append text with variable args into textView */
- (int)textPrintf:(const char*)fmt, ...
{
	va_list		args;
	int			retVal;
	va_start(args, fmt);
	retVal = [self textPrintf:fmt args:args];     
	va_end(args);
	return retVal;
}

/* append text with fprintf(...) style format */
int textPrintf(id self, const char *fmt, ...)
{
	va_list		args;
	int			retVal;
	va_start(args, fmt);
	retVal = [self textPrintf:fmt args:args]; 
	va_end(args);
	return retVal;
}

// --------------------------------------------------------------------------------
// view printing

/* prints the textView */
- print:sender
{
	id		oldInfo, pInfo = [PrintInfo new];
	[pInfo setVertCentered:NO];
	oldInfo = [NXApp setPrintInfo:pInfo];
	[textView lockFocus];
	[textView printPSCode:self];
	[textView unlockFocus];
	[NXApp setPrintInfo:oldInfo];
	[pInfo free];
	return self;
}

/* print textView using enscript */
#define			tempFILE        ".textPrint"
#define			previewFILE     ".previewPrint.ps"
#define			CAT(S)          strcat(command, (S));
- enscriptPrint:(char*)title option:(char*)option printPanel:(BOOL)prtPnl
{
	NXStream	*stream;
	char		temp[256], command[512], *panelStatus;
	int			rtn;
	id			oldInfo;
	id			printPanel = (id)nil;
	id			printInfo = (id)nil;
  
	/* create temp objects */
	printInfo = [PrintInfo new];
	oldInfo = [NXApp setPrintInfo:printInfo];
  
	/* run print panel */
	if (prtPnl)   { printPanel = [PrintPanel new]; rtn = [printPanel runModal]; }
	else          { printPanel = (id)nil;          rtn = NX_OKTAG; }
  
	/* results of print panel */
	if (rtn != NX_CANCELTAG) {
  
		/* save file to disk */
		stream = NXOpenMemory((char*)nil, 0, NX_WRITEONLY);
		[textView writeText:stream];
		NXSaveToFile(stream, tempFILE);
		NXClose(stream);

		/* init default options */
		sprintf(command, "enscript -qhG%d", [printInfo pagesPerSheet]);
		CAT(([printInfo orientation] == NX_LANDSCAPE)? "r": "R");
    
		/* printer option */
		if (rtn == NX_OKTAG) {		// print
			sprintf(temp, " -#%d", [printInfo copies]); CAT(temp);     // # copies
			sprintf(temp, " -P%s", [printInfo printerName]); CAT(temp);// printer name
			panelStatus = "Printing...";
		} else
		if (rtn == NX_SAVETAG) {	// save to file
			sprintf(temp, " -p%s", [printInfo outputFile]); CAT(temp);
			panelStatus = "Saving...";
		} else
		if (rtn == NX_PREVIEWTAG) {	// prieview
			CAT(" -p" previewFILE);
			panelStatus = "Messaging Preview...";
		}

		/* font */
		sprintf(temp, " -f%s%d", [[textView font] name], (int)[[textView font] pointSize]);
		CAT(temp);

		/* options */
		if (title) {sprintf(temp, " -b\"%s\"", title); CAT(temp);}  // title
		if (option) { CAT(" "); CAT(option); }                      // other options
		CAT(" "); CAT(tempFILE);                                    // file name
  
		/* execute print command */
//		if (printPanel) [printPanel->status setStringValue:panelStatus];
		system(command);
    
		/* preview check */
		if (rtn == NX_PREVIEWTAG) system("open " previewFILE);
 
	}

	/* restore state and free objects */
	[NXApp setPrintInfo:oldInfo];
	[printInfo free];
	if (printPanel) [printPanel orderOut:(id)nil];
  
	return self;
}
  
// --------------------------------------------------------------------------------
// executing commands within a shell, using the scroll text as output

/* stop command */
- _stopCommand
{
	int	error;
	DPSRemoveFD(inputDescriptor);
	error = [self _pclose];
	close(inputDescriptor);
	[self commandDidCompleteWithError:error];
	return self;
}

/* set environment variable */
static int _setenv(char **ep, char *eVal, char *fmt, ...)
{
	va_list			args;
	register char	*cp, *dp;
	va_start(args, fmt);
	vsprintf(eVal, fmt, args);
	va_end(args);
	for (;dp = *ep; ep++) {
		for (cp = eVal; (*cp == *dp) && (*cp != '=') && *cp; cp++, dp++) continue;
		if (((*cp == '=') || !*cp) && ((*dp == '=') || !*dp)) { *ep = eVal; return 0; }
	}
	return -1;
}

/* open pipe to shell and execute command */
- (int)_popen:(const char*)cmd cmdPath:(const char*)cmdPath
{
	int			inputP[2], hisOutput, myInput;
	const char	**locEnv = environ;
	
	/* open pipe */
	pipe(inputP);
	myInput = inputP[0];
	hisOutput = inputP[1];
	
	/* fork and execute shell */
	if ((cmdChild = vfork()) == 0) {
		int i;
		char **env, *path, *ePath;
		
		/* make local copy of environment table */
		for (i = 0; locEnv[i]; i++);
		env = (char**)alloca(sizeof(char*) * (i + 1));			// allocate on stack
		memcpy(env, locEnv, sizeof(char*) * (i + 1));
		
		/* check for additional path requirements */
		if (cmdPath && (ePath = getenv("PATH"))) {
			path = (char*)alloca(strlen(ePath) + strlen(cmdPath) + 8);
			_setenv(env, path, "PATH=%s:%s", cmdPath, ePath);
		}
		
		/* set up pipe handles */
		close(myInput);
		if (hisOutput != 1) dup2(hisOutput, 1);
		if (hisOutput != 2) dup2(hisOutput, 2);
		if ((hisOutput != 1) && (hisOutput != 2)) close(hisOutput);
		
		/* execute command */
		setpgrp(0, getpid());
		execle("/bin/csh", "csh", "-f", "-c", cmd, (char *)nil, env);
		_exit(RUNCMD_EXEC);
		
	}
	
	/* set io */
	if (cmdChild == -1)  { close(myInput); myInput = -1; }
	close(hisOutput);
	
	return myInput;
}

/* close shell command pipe */
- (int)_pclose
{
	int			pid, omask, err;
	union wait	status;
	omask = sigblock(sigmask(SIGINT)|sigmask(SIGQUIT)|sigmask(SIGHUP));
	while ((pid = wait(&status)) != cmdChild && pid != -1);
	(void)sigsetmask(omask);
	return (err = (status.w_status & 0xFF))? -1 : (status.w_status >> 8) & 0xFF;
}

/* filter for data piped from command shell */
- (void)_gotData
{
	char	data[1024];
	int		n, cnt = 0;
	BOOL	doScroll = NO;

	/* read available text */
	do {
		if ((n = read(inputDescriptor, data, sizeof(data))) >= 0) {
			cnt += n;
			if (n && [self _appendTextToView:data len:n attr:nullAttr]) doScroll = YES;
			if (delegate && [delegate respondsTo:@selector(runCommandOutput:len:)]) {
				[delegate perform:@selector(runCommandOutput:len:) with:(id)data with:(id)n];
			}
		}
    } while (n == sizeof(data));

	/* show text */
	if (doScroll) [textView scrollSelToVisible];
	if (!cnt) [self _stopCommand];
	
}

/* fd routine for receiving data */
static void gotData(int fd, void *self)
{
	[(ScrollText*)self _gotData];
}

/* execute a command */
- runCommand:(const char*)command withPath:(const char*)cmdPath
{
	inputDescriptor = [self _popen:command cmdPath:cmdPath];
	if (inputDescriptor >= 0) {
		DPSAddFD(inputDescriptor, gotData, self, NX_BASETHRESHOLD);
		return self;
	}
	return (id)nil;
}

/* execute a command */
- runCommand:(const char*)command
{
	return [self runCommand:command withPath:(char*)nil];
}

/* terminate command */
- terminateCommand
{
	if (cmdChild > 0) {
		killpg(cmdChild, SIGTERM);
		kill(cmdChild, SIGTERM);
	}
    return self;
}
  
/* kill command */
- killCommand
{
	if (cmdChild > 0) {
		killpg(cmdChild, SIGKILL);
		kill(cmdChild, SIGKILL);
	}
    return self;
}

// --------------------------------------------------------------------------------
// support for remote shell server output

/* copy text to scrollView (MAIN THREAD ONLY!) */
- (oneway void)commandOutput:(const char*)buffer len:(int)len
{
	if (len && buffer) {
		if (!isMainTHREAD) {
			NXLogError("(ScrollText)commandOutput:len: Not main thread!");
			return;
		}
		if ([self _appendTextToView:buffer len:len attr:nullAttr])
			[textView scrollSelToVisible];
		free((char*)buffer);
	}
}

/* indicate that the shell has completed */
- (oneway void)commandDidCompleteWithError:(int)errorCode;
{
	if (delegate && [delegate respondsTo:@selector(commandDidComplete:withError:)])
		[delegate commandDidComplete:self withError:errorCode];
}

@end
